package com.dp.mallchat.common.user.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.dp.mallchat.common.common.domain.dto.PushMessageDTO;
import com.dp.mallchat.common.common.event.UserOfflineEvent;
import com.dp.mallchat.common.common.event.UserOnlineEvent;
import com.dp.mallchat.common.common.thread.ThreadPoolConfig;
import com.dp.mallchat.common.common.utils.RequestHolder;
import com.dp.mallchat.common.user.dao.UserDao;
import com.dp.mallchat.common.user.domain.user.entity.User;
import com.dp.mallchat.common.user.domain.user.enums.RoleEnum;
import com.dp.mallchat.common.user.domain.ws.dto.WSChannelExtraDTO;
import com.dp.mallchat.common.user.domain.ws.resp.WSBaseResp;
import com.dp.mallchat.common.user.service.LoginService;
import com.dp.mallchat.common.user.service.RoleService;
import com.dp.mallchat.common.user.service.WebSocketService;
import com.dp.mallchat.common.user.service.adapter.WSAdapter;
import com.dp.mallchat.common.websocket.NettyUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.SneakyThrows;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @Author: dupeng
 * @CreateTime: 2024-05-22  11:12
 * @Description: WebSocket 相关实现
 */
@Service
public class WebSocketServiceImpl implements WebSocketService {

    @Autowired
    private WxMpService wxMpService;
    @Autowired
    private UserDao userDao;
    @Autowired
    private LoginService loginService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    @Qualifier(value = ThreadPoolConfig.WS_EXECUTOR)
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;


    //记录在线用户映射通道
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();

    public static final Duration EXPIRE_TIME = Duration.ofHours(1);
    public static final int MAXIMUM_SIZE = 10000;
    //记录所有请求登录的code与channel关系
    private static final Cache<Integer, Channel> CODE_CHANNEL_MAP = Caffeine.newBuilder()
            .expireAfterWrite(EXPIRE_TIME)
            .maximumSize(MAXIMUM_SIZE)
            .build();

    /**
     * 处理所有ws连接的事件
     *
     * @param channel
     */
    @Override
    public void connect(Channel channel) {
        ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
    }


    /**
     * 处理用户扫码请求
     *
     * @param channel
     */
    @SneakyThrows
    @Override
    public void handleLoginReq(Channel channel) {
        //1、生成随机数
        int code = generateCode(channel);
        //2、请求微信带参二维码
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) EXPIRE_TIME.getSeconds());
        //3、返回url
        sendMsg(channel, WSAdapter.buildLoginResp(wxMpQrCodeTicket));
    }

    /**
     * 移除连接
     *
     * @param channel
     */
    @Override
    public void remove(Channel channel) {
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MAP.get(channel);
        Optional<Long> uidOptional = Optional.ofNullable(wsChannelExtraDTO)
                .map(WSChannelExtraDTO::getUid);

        User user = new User();
        user.setId(uidOptional.get());
        user.setLastOptTime(new Date());
        applicationEventPublisher.publishEvent(new UserOfflineEvent(this, user));

        ONLINE_WS_MAP.remove(channel);
    }


    /**
     * 走登录成功逻辑，通过code找到channel推送消息
     *
     * @param code
     * @param uid
     */
    @Override
    public void scanLoginSuccess(Integer code, Long uid) {
        //确认用户是否在本机上
        Channel channel = CODE_CHANNEL_MAP.getIfPresent(code);
        if (Objects.isNull(channel)) {
            return;
        }
        //

        User user = userDao.getById(uid);

        //1、移除code与channel的映射关系
        CODE_CHANNEL_MAP.invalidate(code);

        //2、调用登录模块获取token
        String token = loginService.getToken(uid);

        //3、推送登录成功消息
//        sendMsg(channel, WSAdapter.buildResp(user, token));
        // 认证成功
        success(channel, token, user);
    }

    /**
     * 等待用户授权
     *
     * @param code
     */
    @Override
    public void waitAuthorization(Integer code) {
        Channel channel = CODE_CHANNEL_MAP.getIfPresent(code);
        if (Objects.isNull(channel)) {
            //长时间未进行授权，认证有效期已过
            return;
        }
        sendMsg(channel, WSAdapter.buildWaitAuthorizationResp());
    }

    /**
     * token认证
     *
     * @param channel
     * @param token
     */
    @Override
    public void authorization(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.isNull(uid)) {
            //token无效，需要重新登录
            sendMsg(channel, WSAdapter.buildAuthorizationFailResp());
            return;
        }
        User user = userDao.getById(uid);

        // 认证成功
        success(channel, token, user);
    }

    /**
     * 推送所有在线用户
     *
     * @param message 消息
     * @param skipUid 需要跳过的人
     */
    @Override
    public void sendToAllOnlineUser(WSBaseResp<?> message, Long skipUid) {
        ONLINE_WS_MAP.forEach((channel, extraDTO) -> {
            if (Objects.nonNull(skipUid) && Objects.equals(extraDTO.getUid(), skipUid)) {
                return;
            }
            // 异步推送
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, message));
        });
    }

    @Override
    public void sendToUid(WSBaseResp<?> wsBaseMsg, Long uid) {
        //TODO 无法定向推送用户，暂时遍历
        ONLINE_WS_MAP.forEach((channel, extraDTO) -> {
            if (Objects.equals(uid, extraDTO.getUid())) {
                sendMsg(channel, wsBaseMsg);
            }
        });
    }

    private void success(Channel channel, String token, User user) {
        //更新上线列表
        online(channel, user.getId());
        //用户是否为超管
        boolean isPower = roleService.hasPower(user.getId(), RoleEnum.ADMIN);
        //返回用户信息
        sendMsg(channel, WSAdapter.buildResp(user, token, isPower));

        // 解析用户ip归属地事件
        user.setLastOptTime(new Date());
        user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));

        //todo 用户上线后续事件
    }

    /**
     * 用户上线
     * @param channel
     * @param uid
     */
    private void online(Channel channel, Long uid) {
        getOrInitChannelExt(channel).setUid(uid);
    }

    /**
     * 如果在线列表不存在，就先把该channel放进在线列表
     *
     * @param channel
     * @return
     */
    private WSChannelExtraDTO getOrInitChannelExt(Channel channel) {
        WSChannelExtraDTO wsChannelExtraDTO =
                ONLINE_WS_MAP.getOrDefault(channel, new WSChannelExtraDTO());
        WSChannelExtraDTO old = ONLINE_WS_MAP.putIfAbsent(channel, wsChannelExtraDTO);
        return ObjectUtil.isNull(old) ? wsChannelExtraDTO : old;
    }

    /**
     * ws回传前端消息
     *
     * @param channel
     * @param wsBaseResp
     */
    private void sendMsg(Channel channel, WSBaseResp<?> wsBaseResp) {
        System.out.println("推送消息：" + JSONUtil.toJsonStr(wsBaseResp));
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }

    /**
     * 生成随机码
     *
     * @param channel
     * @return
     */
    private int generateCode(Channel channel) {
        //生成随机数，判断Map中是否存在，存在则生成新的继续插入Map
        int code;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        } while (ObjectUtil.isNotEmpty(CODE_CHANNEL_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }
}
